package edu.pitt.cs.cs2510.project3.model;

/*
 * File: WorkerServer,java
 * Authors: Jesse Szwedko, Callen Shaw, Heather Friedberg
 * CS2510 Project 3
 */

import java.nio.BufferOverflowException;
import java.util.*;

import edu.pitt.cs.cs2510.project3.configuration.InputParameters;
import edu.pitt.cs.cs2510.project3.model.replication.CopyReplication;
import edu.pitt.cs.cs2510.project3.model.replication.DeleteReplication;
import edu.pitt.cs.cs2510.project3.model.replication.Replication;


public class WorkerServer extends Server {
	private LinkedList<File> currentReplications;

	/**
	 * Pending outgoing replications
	 */
	private Map<String, List<Integer>> pendingReplications;


	private FrontEnd frontEnd;

	public boolean die = false;
	public boolean isShuttingDown = false;

	protected List<String> requestedFilenames;

	final float BlockConsumptionTime;

	public WorkerServer(int id, InputParameters simulationParameters, FrontEnd frontEnd, Object monitor) {
		super(id, simulationParameters, monitor);
		this.currentReplications = new LinkedList<File>();
		this.pendingReplications = new HashMap<String, List<Integer>>();
		this.frontEnd = frontEnd;
		BlockConsumptionTime = (float)simulationParameters.getBlockConsumeTime();
	}

	//returns the load of this WorkerServer. It is not overloaded if the value < 1
	public float getLoad() {
		return (float)(requests.size() / simulationParameters.getBlockConsumeTime()); 
	}

	@Override
	public void run()
	{
		while (!this.die)
		{			
			//we aren't ready for the next clock cycle yet so sleep a bit and try again
			if (!this.tick)
			{
				try {
					synchronized(this.frontEnd) {
						this.frontEnd.wait();
					}
				} catch (InterruptedException e1) {
					e1.printStackTrace();
				}
				continue;
			}

			if (this.process())
			{
				updateState();

				this.tick = false;
			}
		}

	}

	protected void startReplication (File file, WorkerServer server) {
		this.sendReplication(file, server);
	}

	/*
	 * Processes the request queue
	 * @return True if we are done processing for this clock tick, i.e. has serviced a request
	 */
	protected boolean process() {

		if (this.requests.isEmpty())
		{					
			return true;
		}

		//TODO service in some order (does round robin, skipping full buffers)

		Iterator<Request> iterator = this.requests.iterator();
		Request request;
		boolean completed = false;
		boolean found = false;
		request = iterator.next();
		while (true) {
			if (request.client.getBufferSize(request) > 0) {
				completed = this.processRequest(request);
				found = true;
				break;
			}
			else {
				if (iterator.hasNext())
				{
					request = iterator.next();
					continue;
				}
				else
				{
					request = null;
					break;
				}
			}
		}

		if (found) {
			this.requests.remove(request);
			if (completed) {
				this.completedRequests.add(request);
			}
			else {
				this.requests.add(request);
			}
		}

		return true;
	}

	@Override
	protected double getUptimeCost() {
		return this.simulationParameters.getUptimeCost();
	}


	/**
	 * @param request The request
	 * @return True if the process is finished, false if it still has more blocks to transfer
	 */
	private boolean processRequest(Request request) {
		//System.out.println("Block " + request.currentBlockId + " of request: " + request + " serviced");

		request.requestCost += this.simulationParameters.getDistance(request.client.id, this.frontEnd.id);

		FileBlock fileBlock = new FileBlock(this.files.get(request.fileId), request.currentBlockId, this.simulationParameters.getDistance(request.client.id, this.frontEnd.id), this.simulationParameters.getBlockConsumeTime());

		request.client.addFileBlock(request, fileBlock);
		request.currentBlockId++;

		return request.currentBlockId > request.size;
	}

	@Override
	public boolean isDone() {
		return !this.tick && this.requests.isEmpty();
	}

	@Override
	public boolean isReady() {
		return !this.tick;
	}

	@Override
	public String toString() {
		return "WorkerServer [die=" + die + ", noMoreRequests="
		+ noMoreRequests + ", requestedFilenames="
		+ requestedFilenames + ", id=" + id + ", files=" + files
		+ ", requests=" + requests + ", completedRequests="
		+ completedRequests + ", replications=" + replications
		+ ", time=" + time + ", tick=" + tick + "]";
	}

	@Override
	public void noMoreRequests(boolean noMoreRequests) {
		this.noMoreRequests = noMoreRequests;			
	}

	public void die()
	{
		this.die = true;
	}

	@Override
	public String getUniqueIdentifier() {
		return this.frontEnd.getUniqueIdentifier() + ":" + this.id;
	}

	public synchronized void wakeUp() {
		this.notify();
	}

	public Integer[] getFileIds() {
		ArrayList<Integer> ret = new ArrayList<Integer>(files.keySet().size());
		ret.addAll(files.keySet());
		return ret.toArray(new Integer[ret.size()]);
	}

	public File getFile(int idx) {
		if(files.get(idx) == null)
			throw new IllegalArgumentException(idx + " not a file on this WorkerServer");
		return files.get(idx);
	}

	public void deleteFile(File file) {
		if(file != files.remove(file.fileId))
			throw new IllegalStateException("deletedFile " + file + " but wasn't the same as the Server's file. This a bug.");
	}

	public List<CopyReplication> getCopies() {
		List<CopyReplication> copies = new ArrayList<CopyReplication>();
		for (Replication replication : this.replications)
		{
			if (replication instanceof CopyReplication)
			{
				copies.add((CopyReplication) replication);
			}
		}
		return copies;
	}
	public List<DeleteReplication> getDeletions() {
		List<DeleteReplication> deletions = new ArrayList<DeleteReplication>();
		for (Replication replication : this.replications)
		{
			if (replication instanceof DeleteReplication)
			{
				deletions.add((DeleteReplication) replication);
			}
		}
		return deletions;
	}

	/**
	 * Sends a replica to another server
	 * @param file File to send
	 * @param server Recipient server
	 */
	protected boolean sendReplication(File file, WorkerServer server) {

		file.timeInTransit = this.getServerDistance(this, server);

		if (!this.pendingReplications.containsKey(server.id))
		{
			this.pendingReplications.put(server.getUniqueIdentifier(), new ArrayList<Integer>());
		}

		this.pendingReplications.get(server.getUniqueIdentifier()).add(file.fileId);

		File newFile = file.clone();
		return server.addReplication(newFile);
	}

	protected void doneReplicating(File file, WorkerServer server) {
		server.removeReplication(file, this);
	}

	private synchronized void removeReplication(File file, WorkerServer server) {
		List<Integer> files = this.pendingReplications.get(server.id);

		if (files != null)
		{
			if (files.contains(file.fileId))
			{
				files.remove(file.fileId);
			}

			if (files.isEmpty())
			{
				this.pendingReplications.remove(server.id);
			}
		}
	}

	private synchronized boolean addReplication(File file) {
		if (this.files.size() > this.remainingSpace())
		{
			return false;
		}

		file.numBlocks = 0;

		this.files.put(file.fileId, file);

		this.currentReplications.add(file);
		this.wakeUp();
		return true;
	}

	/**
	 * Determine whether the given file is needed
	 * @param file The file to be deleted
	 * @return True if the file can be deleted
	 */
	public boolean canDelete(File file)
	{
		for (String serverId : this.pendingReplications.keySet())
		{
			if (this.pendingReplications.get(serverId).contains(file.fileId))
			{
				return false;
			}
		}
		if(currentReplications.contains(file)) {
			return false;
		}
		Iterator<Request> ri = requests.iterator();
		while(ri.hasNext())
			if(ri.next().fileId == file.fileId)
				return false;
		return !file.master;
	}

	public void updateState() {		
		for (File file : this.currentReplications)
		{
			if (file.timeInTransit > 0)
				file.timeInTransit--;
		}

		if (this.currentReplications.isEmpty())
		{
			return;
		}

		File file;
		do {
			file = this.currentReplications.poll();

			if (file.timeInTransit > 0)
			{
				this.currentReplications.add(file);
			}
		} while(file.timeInTransit > 0);

		file.numBlocks++;
		int feBlocks = frontEnd.files.get(file.fileId).numBlocks;
		frontEnd.files.get(file.fileId).numBlocks = Math.max(feBlocks, file.numBlocks);

		if (file.numBlocks >= file.size)
		{
			this.frontEnd.files.get(file.fileId).touch(this.time);
			return;
		}

		this.currentReplications.add(file);
	}
	
	public boolean isReplicating(){
		return !pendingReplications.isEmpty();
	}
}
